<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>HTML5 Canvas Clock</title>
	<style>
		* {
			margin:0;
			outline:none;
			padding:0;
		}
		body {
			background:#000;
			font-family:'Lucida Grande','Helvetica','Arial';
			font-size:10px;
			overflow:hidden;
		}
		canvas {
			background:#222;
			cursor:default;
			z-index:1;
		}
		.nope {
			color:#fff;
			text-align:center;
			margin-top:150px;
		}
		header {
			position:relative;
			text-shadow:1px 1px 0px rgba(0,0,0,0.5);
			text-transform:uppercase;
			width:100%;
			z-index:10;
		}
		#about {
			color:#fff;
			color:rgba(255,255,255,0.5);
			display:block;
			float:right;
			margin:20px;
			text-align:right;
			width:50%;
		}
		h1 {
			color:rgba(255,255,255,0.75);
			float:left;
			font-size:10px;
			font-weight:normal;
			margin:20px;
		}
		a {
			color:rgba(255,255,255,0.5);
			display:inline-block;
			text-decoration:none;
			transition:0.5s ease color;
			-moz-transition:0.5s ease color;
			-o-transition:0.5s ease color;
			-webkit-transition:0.5s ease color;
		}
		a:hover {
			color:rgba(255,255,255,0.75);
		}
		ul#options {
			list-style:none;
			margin:10px 0 0;
			position:relative;
			right:0;
			z-index:10;
		}
		ul#options li {
			margin:5px 0;
			min-width:200px;
			opacity:0;
			transition:0.25s ease-in opacity;
			-moz-transition:0.25s ease-in opacity;
			-o-transition:0.25s ease-in opacity;
			-webkit-transition:0.25s ease-in opacity;
		}
		ul#options li.group {
			margin-top:15px;
		}
		ul#options li * {
			display:none;
		}
		ul#options li a {
			box-shadow:1px 1px 3px rgba(0,0,0,0.25);
			background-color:rgba(0,0,0,0.5);
			border-radius:3px;
			padding:3px 5px;
			position:relative;
			transition:0.5s ease all;
			-moz-border-radius:3px;
			-o-box-shadow:1px 1px 3px rgba(0,0,0,0.25);
			-moz-box-shadow:1px 1px 3px rgba(0,0,0,0.25);
			-webkit-box-shadow:1px 1px 3px rgba(0,0,0,0.25);
			-moz-transition:0.5s ease all;
			-o-transition:0.5s ease all;
			-webkit-transition:0.5s ease all;
		}
		ul#options li a:hover {
			color:rgba(255,255,255,0.75);
		}
		ul#options li a.on {
			background-color:rgba(255,255,255,0.8);
			color:rgba(0,0,0,0.9);
			text-shadow:0px 0px 0px;
		}
		ul#options li a.on:after {
			content:"\2713 ";
		}
		ul#options.on li {
			opacity:1;
			right:20px;
		}
		ul#options.on li * {
			display:inline-block;
		}
		ul#borders {
		}ul#borders li {
			 position:fixed;
			 list-style:none;
			 margin:0;
			 background-color:transparent;
			 background-color:rgba(0,0,0,0.05);
			 z-index:100;
		 }
		li#top {
			height:10px;
			left:0;
			right:0;
			top:0;
		}
		li#right {
			bottom:10px;
			right:0;
			top:10px;
			width:10px;
		}
		li#bottom {
			bottom:0;
			height:10px;
			left:0;
			right:0;
		}
		li#left {
			bottom:10px;
			left:0;
			top:10px;
			width:10px;
		}
	</style>
</head>
<body>
<canvas width="1920" height="500" id="canvas" style="position: absolute; left: 0px; top: 0px;"><p class="nope">No canvas, no particles</p></canvas>
<header>
	<h1>Particle Clock</h1>
	<div id="about">
		<a href="https://www.html5tricks.com/demo/html5-canvas-pixel-clock/index2.html#" id="toggle-options">[+] Show Options</a>
		<ul id="options">
			<li><a href="https://www.html5tricks.com/demo/html5-canvas-pixel-clock/index2.html#" id="quivers" class="">Quiver</a></li>
			<li><a href="https://www.html5tricks.com/demo/html5-canvas-pixel-clock/index2.html#" id="gradient" class="on">Gradient</a></li>
			<li><a href="https://www.html5tricks.com/demo/html5-canvas-pixel-clock/index2.html#" id="color" class="on">Colorize</a></li>
			<li><a href="https://www.html5tricks.com/demo/html5-canvas-pixel-clock/index2.html#" id="valentineify" class="">Valentine-ify</a></li>
			<li class="group"><span>Mouse down: explode and repel</span></li>
			<li><span>Mouse down + shift: explode and attract</span></li>
			<li><span>Arrow Up: increase particle size</span></li>
			<li class="group"><span>Sorry about your CPU</span></li>
			<li><span id="fps">FPS Average: 2 Current: 18</span></li>
		</ul>
	</div>
</header>
<script>
	var Clock = (function() {
		// private variables
		var canvas, // canvas element
			ctx, // canvas context
			bgGrad = true, // background gradient flag
			gradient, // gradient (background)
			height = 400, // canvas height
			key = {up: false, shift: false}, // key presses
			particles = [], // particle array
			particleColor = 'hsla(0, 0%, 100%, 0.3)', // particle color
			mouse = {x: 0, y: 0}, // position of mouse / touch
			press = false, // pressed flag
			quiver = false, // quiver flag
			text, // the text to copy pixels from
			textSize = 140, // (initial) textsize
			valentine = false, // valentine-ify it for a bit?
			msgTime = 100, // time to show a message before returning to clock
			updateColor = true, // update color of gradient / particles with time?
			width = 800; // canvas width
		// Constants
		var FRAME_RATE = 20, // frames per second target
			MIN_WIDTH = 800, // minimum width of canvas
			MIN_HEIGHT = 400, // minimum height of canvas
			PARTICLE_NUM = 600, // (max) number of particles to generate
			RADIUS = Math.PI * 2; // radius of particle
		var defaultStyles = function() {
			textSize = 140;
			// particle color
			particleColor = 'hsla(0, 0%, 100%, 0.3)';
			// color stops
			var gradientStops = {
				0: '#333333',
				0.5: '#222222'
			};
			// create gradient
			setGradient(gradientStops);
		};
		var draw = function(p) {
			ctx.fillStyle = particleColor;
			ctx.beginPath();
			ctx.arc(p.x, p.y, p.size, 0, RADIUS, true);
			ctx.closePath();
			ctx.fill();
		};
		var explode = function() {
			for(var i = 0, l = particles.length; i < l; i++) {
				var p = particles[i];
				if(p.inText) {
					var ax = mouse.x - p.px,
						ay = mouse.y - p.py,
						angle = Math.atan2(ay, ax),
						polarity,
						C = Math.cos(angle),
						S = Math.sin(angle);
					// change polarity
					// attract particles if mouse pressed, repel if shift + mousedown
					polarity = (key.shift === true) ? -1 : 1;
					p.x += polarity * (Math.pow((C-1), 2) -1) + p.velocityX * p.delta;
					p.y += polarity * (Math.pow((S-1), 2) -1) + p.velocityY * p.delta;
					// set previous positions
					p.px = p.x;
					p.py = p.y;
					draw(p);
				}
			}
		};
		var getTime = function(amPM) {
			var date = new Date(),
				hours = date.getHours(),
				timeOfDay = '';
			if(amPM) {
				hours = ( hours > 12 ) ? hours -= 12 : hours;
				hours = ( hours == 0 ) ? 12 : hours;
			} else {
				hours = pad(hours);
			}
			var minutes = pad(date.getMinutes());
			var seconds = pad(date.getSeconds());
			return {
				hours: hours,
				minutes: minutes,
				seconds: seconds,
				timeString: hours + " : " + minutes + " : " + seconds
			};
		};
		// animation loop
		var loop = function() {
			// clear out text
			ctx.clearRect(0, 0, width, height);
			var time = getTime(true);
			textSize = 140;
			// draw text on canvas
			if(valentine === true) {
				if(msgTime > 0) {
					textSize = 180;
					text = '?';
					msgTime--;
				} else {
					text = time.timeString;
				}
				// valentine-ify it by setting hue to pink
				setStyles(300);
			} else if(updateColor === true && bgGrad === true) {
				// changing color with time
				// @TODO: come up with something better, this is a hacky implementation
				var color = time.hours + time.minutes + time.seconds;
				setStyles(color);
				text = time.timeString;
			} else {
				defaultStyles();
				text = time.timeString;
			}
			ctx.fillStyle = "rgb(255, 255, 255)";
			ctx.textBaseline = "middle";
			ctx.font = textSize + "px 'Avenir', 'Helvetica Neue', 'Arial', 'sans-serif'";
			ctx.fillText(text, (width - ctx.measureText(text).width) * 0.5, height * 0.5);
			// copy pixels
			var imgData = ctx.getImageData(0, 0, width, height);
			// clear canvas, again
			ctx.clearRect(0, 0, width, height);
			if(bgGrad === true) {
				// draw gradient
				ctx.fillStyle = gradient;
				ctx.fillRect(0, 0, width, height);
			}
			if(press === false) {
				// reset particles
				for(var i = 0, l = particles.length; i < l; i++) {
					var p = particles[i];
					p.inText = false;
				}
				particleText(imgData);
			} else {
				explode();
			}
			FPS.update('fps');
		};
		var pad = function(number) {
			return ('0' + number).substr(-2);
		};
		var particleText = function(imgData) {
			var pxls = [];
			for(var w = width; w > 0; w-=6) {
				for(var h = 0; h < width; h+=6) {
					var index = (w+h*(width))*4;
					if(imgData.data[index] > 10) {
						pxls.push([w, h]);
					}
				}
			}
			var count = pxls.length;
			for(var i = 0; i < pxls.length && i < particles.length; i++) {
				try {
					var p = particles[i],
						X,
						Y;
					if(quiver) {
						X = (pxls[count-1][0]) - (p.px + Math.random() * 5);
						Y = (pxls[count-1][1]) - (p.py + Math.random() * 5);
					} else {
						X = (pxls[count-1][0]) - p.px;
						Y = (pxls[count-1][1]) - p.py;
					}
					// tangent
					var T = Math.sqrt(X*X + Y*Y);
					// arctangent
					var A = Math.atan2(Y, X);
					// cosine
					var C = Math.cos(A);
					// sine
					var S = Math.sin(A);
					// set new postition
					p.x = p.px + C * T * p.delta;
					p.y = p.py + S * T * p.delta;
					// set previous positions
					p.px = p.x;
					p.py = p.y;
					p.inText = true;
					// draw the particle
					draw(p);
					if(key.up === true) {
						p.size += 0.3;
					} else {
						var newSize = p.size - 0.5;
						if(newSize > p.origSize && newSize > 0) {
							p.size = newSize;
						} else {
							p.size = m.origSize;
						}
					}
				} catch(e) {
					//console.log(e);
				}
				count--;
			}
		};
		var setCoordinates = function(e) {
			if(e.offsetX) {
				return { x: e.offsetX, y: e.offsetY }; // use offset if available
			} else if (e.layerX) {
				return { x: e.layerX, y: e.layerY }; // firefox... make sure to position the canvas
			} else {
				// iOS. Maybe others too?
				return { x: e.pageX - canvas.offsetLeft, y: e.pageY - canvas.offsetTop };
			}
		};
		// set dimensions of canvas
		var setDimensions = function () {
			width = Math.max(window.innerWidth, MIN_WIDTH);
			height = Math.max(window.innerHeight, MIN_HEIGHT);
			// Resize the canvas
			canvas.width = width;
			canvas.height = height;
			canvas.style.position = 'absolute';
			canvas.style.left = '0px';
			canvas.style.top = '0px';
		};
		var setGradient = function(gradientStops) {
			// create gradient
			gradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width);
			// iterate through colorstops
			for (var position in gradientStops) {
				var color = gradientStops[position];
				gradient.addColorStop(position, color);
			}
		};
		var setStyles = function(hue) {
			// color stops
			var gradientStops = {
				0: 'hsl(' + hue + ', 100%, 100%)',
				0.5: 'hsl(' + hue +', 10%, 50%)'
			};
			// change particle color
			particleColor = 'hsla(' + hue + ', 10%, 50%, 0.3)';
			// create gradient
			setGradient(gradientStops);
		};
		/**
		 * Public Methods
		 */
		return {
			init: function(canvasID) {
				canvas = document.getElementById(canvasID);
				// make sure canvas exists and that the browser understands it
				if(canvas === null || !canvas.getContext) {
					return;
				}
				// set context
				ctx = canvas.getContext("2d");
				// set dimensions
				setDimensions();
				// ui
				this.ui();
				for(var i = 0; i < PARTICLE_NUM; i++) {
					particles[i] = new Particle(canvas);
				}
				// show FPS
				FPS.initialize(canvas, 'fps');
				// set defaults
				defaultStyles();
				// let's do this
				setInterval(loop, FRAME_RATE);
			},
			ui: function() {
				// UI: buttons and events
				var toggleOptions = document.getElementById('toggle-options'),
					options = document.getElementById('options'),
					onMsg = '[-] Hide Options',
					offMsg = '[+] Show Options',
					quiverBtn = document.getElementById('quivers'),
					gradientBtn = document.getElementById('gradient'),
					valentineifyBtn = document.getElementById('valentineify'),
					colorBtn = document.getElementById('color');
				toggleOptions.innerHTML = offMsg;
				/**
				 * Events
				 */
				toggleOptions.addEventListener('click', function(e) {
					e.preventDefault();
					if(options.className === 'on') {
						options.className = '';
						toggleOptions.innerHTML = offMsg;
					} else {
						options.className = 'on';
						toggleOptions.innerHTML = onMsg;
					}
				}, false);
				quiverBtn.addEventListener('click', function(e) {
					e.preventDefault();
					if(quiverBtn.className === 'on') {
						quiverBtn.className = '';
						quiver = false;
					} else {
						quiverBtn.className = 'on';
						quiver = true;
					}
				}, false);
				gradientBtn.addEventListener('click', function(e) {
					e.preventDefault();
					if(gradientBtn.className === 'on') {
						gradientBtn.className = '';
						bgGrad = false;
					} else {
						gradientBtn.className = 'on';
						bgGrad = true;
					}
				}, false);
				valentineifyBtn.addEventListener('click', function(e) {
					e.preventDefault();
					if(valentineifyBtn.className === 'on') {
						valentineifyBtn.className = '';
						valentine = false;
						msgTime = 0;
					} else {
						valentineifyBtn.className = 'on';
						msgTime = 60;
						valentine = true;
					}
				}, false);
				colorBtn.addEventListener('click', function(e) {
					e.preventDefault();
					if(colorBtn.className === 'on') {
						colorBtn.className = '';
						updateColor = false;
					} else {
						colorBtn.className = 'on';
						updateColor = true;
					}
				}, false);
				document.addEventListener('keydown', function(e) {
					switch(e.keyCode ) {
						case 16: // shift
							key.shift = true;
							e.preventDefault();
							break;
						case 38: // up key
							key.up = true;
							e.preventDefault();
							break;
					}
				}, false);
				document.addEventListener('keyup', function(e) {
					switch(e.keyCode ) {
						case 16: // shift
							key.shift = false;
							e.preventDefault();
							break;
						case 38: // space
							key.up = false;
							e.preventDefault();
							break;
					}
				}, false);
				window.addEventListener('resize', function(e){
					setDimensions();
				}, false);
				canvas.addEventListener('mousedown', function(e){
					press = true;
				}, false);
				document.addEventListener('mouseup', function(e){
					press = false;
				}, false);
				canvas.addEventListener('mousemove', function(e) {
					if(press) {
						mouse = setCoordinates(e);
					}
				}, false);
				// @TODO: add touch events
			}
		};
	})();
	// Create new particles
	var Particle = function(canvas) {
		var range = Math.random() * 180 / Math.PI, // random starting point
			spread = canvas.height, // how far away from text should the particles begin?
			size = Math.random() * 7; // random size of particle
		this.delta = 0.25;
		this.x = 0;
		this.y = 0;
		// starting positions
		this.px = (canvas.width / 2) + (Math.cos(range) * spread);
		this.py = (canvas.height / 2) + (Math.sin(range) * spread);
		this.velocityX = Math.floor(Math.random() * 10) - 5;
		this.velocityY = Math.floor(Math.random() * 10) - 5;
		this.size  = size;
		this.origSize = size;
		this.inText = false;
	};
	var FPS = {
		// defaults
		delta: 0,
		lastTime: 0,
		frames: 0,
		totalTime: 0,
		updateTime: 0,
		updateFrames: 0,
		initialize: function(canvasID, fpsID) {
			this.lastTime = (new Date()).getTime();
			if(!document.getElementById(fpsID) && document.getElementById(canvasID)) {
				this.createFPS(canvasID, fpsID);
			}
		},
		// create FPS div if needed
		createFPS: function(canvasID, fpsID) {
			var div = document.createElement('div');
			div.setAttribute('id', fpsID);
			var canvas = document.getElementById(canvasID);
			var parent = canvas.parentNode;
			div.innerHTML = "FPS AVG: 0 CURRENT: 0";
			parent.appendChild(div);
		},
		// update FPS count
		update: function(fpsID) {
			var now = (new Date()).getTime();
			this.delta = now - this.lastTime;
			this.lastTime = now;
			this.updateTime += this.delta;
			this.totalTime += this.delta;
			this.frames++;
			this.updateFrames++;
			document.getElementById(fpsID).innerHTML = "FPS Average: " + Math.round(1000 * this.frames / this.  totalTime) + " Current: " + Math.round(1000 * this.updateFrames / this.updateTime);
			this.updateTime = 0; // reset time
			this.updateFrames = 0; // reset frames
		}
	};
	Clock.init('canvas');
</script>
</body>
</html>